﻿
using Slock.Components;
using Slock.Utility;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System;
using Microsoft.Xna.Framework;
using Slock.Rendering;
using Slock.GameObjects.ActorComponents;

namespace Slock.GameObjects
{
    /// <summary>
    /// A small serializable region of blocks in the block world
    /// </summary>
    public class TerrainPatch : Actor, IDisposable
    {
        private bool isGeometryDirty = true;
        private Point3 patchLoc;
        StaticMeshRendererComponent renderer;
        BoundingBoxCollisionComponent cullTest;

        BlockInfo[,,] blocks = new BlockInfo[WorldInfo.Terrain_Patch_Dimension.X, WorldInfo.Terrain_Patch_Dimension.Y, WorldInfo.Terrain_Patch_Dimension.Z];

        /// <summary>
        /// Initializes a new instance of terrain patch
        /// </summary>
        /// <param name="world"></param>
        public TerrainPatch(World world, Point3 patchLocation) : base(world)
        {
            var collision = new BoundingBoxCollisionComponent(this,
                new Vector3(this.Position.X - (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.X / 2), this.Position.Y - (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.Y / 2),
                    this.Position.Z - (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.Z / 2)),
                new Vector3(this.Position.X + (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.X / 2), this.Position.Y + (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.Y / 2),
                    this.Position.Z + (WorldInfo.BLOCK_SIZE * WorldInfo.Terrain_Patch_Dimension.Z / 2)));

            var mesh = new StaticMeshRendererComponent(this);
            this.AddComponent(ActorInfo.OCCLUSION_COMPONENT, collision);
            this.AddComponent(ActorInfo.RENDER_COMPONENT, mesh);

            this.renderer = mesh;
            this.cullTest = collision;

            this.patchLoc = patchLocation;

            for (int x = 0; x < WorldInfo.Terrain_Patch_Dimension.X; x++)
                for (int y = 0; y < WorldInfo.Terrain_Patch_Dimension.Y; y++)
                    for (int z = 0; z < WorldInfo.Terrain_Patch_Dimension.Z; z++)
                        blocks[x, y, z] = new BlockInfo() { FaceMask = 0, Type = BlockTypes.None };
                    
        }

        /// <summary>
        /// Used to rebuild the DynamicVertex and DynamicIndex Buffers
        /// </summary>
        public override void Reset()
        {
            BuildGeometry(true);
        }

        #region Methods

        /// <summary>
        /// Reconstructs geometry buffers and culls unseen blocks from view
        /// </summary>
        /// <param name="forceBuild">Forces a geometry build even if the vertex buffers are not stale</param>
        public void BuildGeometry(bool forceBuild)
        {
            if (isGeometryDirty || forceBuild)
                isGeometryDirty = false;

            else if (!forceBuild)
                return;

            // we've gotta dispose our buffers before we make new ones
            this.Dispose();

            VertexBuffer vertexBuffer;
            IndexBuffer indexBuffer;

            List<VertexPositionNormalTexture> vertTemp = new List<VertexPositionNormalTexture>(512);
            List<short> indexTemp = new List<short>(512);
            int blockCount = 0;
            for (int x = 0; x < blocks.GetLength(0); x++)
            {
                for (int y = 0; y < blocks.GetLength(1); y++)
                {
                    for (int z = 0; z < blocks.GetLength(2); z++)
                    {
                        var block = blocks[x, y, z];

                        if (block.Type == BlockTypes.None)
                            continue;
                        else
                        {
                            // Retrieve the geometry from it
                            VertexPositionNormalTexture[] verts;
                            short[] indices;
                            MaskBlock(ref block, x, y, z);
                            GetGeometry(ref block, out indices, out verts);

                            vertTemp.AddRange(verts);

                            for (int index = 0; index < indices.Length; index++)
                            {
                                indices[index] += (short)(blockCount * verts.Length);
                                indexTemp.Add(indices[index]);
                            }

                            blockCount++;
                        }
                    }
                }
            }

            if (vertTemp.Count > 0)
            {
                vertexBuffer = new VertexBuffer(World.GraphicsDevice, typeof(VertexPositionNormalTexture), vertTemp.Count, BufferUsage.WriteOnly);
                indexBuffer = new IndexBuffer(World.GraphicsDevice, typeof(short), indexTemp.Count, BufferUsage.WriteOnly);

                vertexBuffer.SetData(vertTemp.ToArray());
                indexBuffer.SetData(indexTemp.ToArray());

                this.renderer.Initalize(ref vertexBuffer, ref indexBuffer);
                this.cullTest.BoundingBox = BoundingBox.CreateFromPoints(vertTemp.ConvertAll(output => output.Position));
                this.renderer.Enabled = true;
                GC.Collect();
            }
            else
                this.renderer.Enabled = false;
        }

        /// <summary>
        /// Looks around the block for others that might obscure its faces
        /// </summary>
        /// <param name="block"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        private void MaskBlock(ref BlockInfo block, int x, int y, int z)
        {
            BlockFace mask = block.FaceMask;

            for (int side = 0; side < 6; side++)
            {
                // for every side, make sure the adjacent block gets checked
            }
        }

        /// <summary>
        /// Returns a vertex and index buffer containing the blocks visible faces
        /// </summary>
        /// <param name="block"></param>
        /// <param name="indices"></param>
        /// <param name="verts"></param>
        private void GetGeometry(ref BlockInfo block, out short[] indices, out VertexPositionNormalTexture[] verts)
        {

        }

        /// <summary>
        /// Updates a block with a new type at the specified position
        /// </summary>
        /// <param name="location"></param>
        /// <param name="type"></param>
        public void UpdateBlockPosition(Point3 location, BlockTypes type)
        { this.blocks[location.X, location.Y, location.Z].Type = type; }

        /// <summary>
        /// Updates a block with a new type at the specified position
        /// </summary>
        /// <param name="location"></param>
        /// <param name="type"></param>
        public void UpdateBlockPosition(int x, int y, int z, BlockTypes type)
        { this.blocks[x, y, z].Type = type; }


        /// <summary>
        /// Disposes of the renderer
        /// </summary>
        public void Dispose()
        {
            if (renderer != null)
                renderer.Dispose();
        }

        #endregion

        #region Update/Draw

        /// <summary>
        /// 
        /// </summary>
        /// <param name="delta"></param>
        protected override void Tick(float delta)
        {
            base.Tick(delta);

            if (this.isDirty)
                BuildGeometry(false);
        }

        /// <summary>
        /// Draws debug mesh for the terrain patch cull box
        /// </summary>
        /// <param name="device"></param>
        public void DrawDebug(GraphicsDevice device)
        {
            VertexPositionColor[] verts;
            short[] indices;

            Helpers.BoundingBoxDebugMesh(cullTest.BoundingBox, out verts, out indices, Color.Red);

            device.DrawUserIndexedPrimitives(PrimitiveType.LineList, verts, 0, verts.Length, indices, 0, indices.Length / 2);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets a specific block from the terrain patch
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        public BlockInfo this[int x, int y, int z]
        { get { return this.blocks[x, y, z]; } }

        /// <summary>
        /// Get if the geometry in this patch needs to be rebuilt
        /// </summary>
        public bool isDirty
        { get { return this.isGeometryDirty; } }

        /// <summary>
        /// Get Array dimensions as a point 3
        /// </summary>
        public Point3 ArrayDimensions
        { get { return new Point3(this.blocks.GetLength(0), this.blocks.GetLength(1), this.blocks.GetLength(2)); } }

        /// <summary>
        /// Get this patch's location in the world patch array
        /// </summary>
        public Point3 PatchLocation
        { get { return this.patchLoc; } }

        #endregion
    }
}
